﻿/************************************************************************************ 
 * Copyright (c) 2008-2010, Columbia University
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the Columbia University nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY COLUMBIA UNIVERSITY ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <copyright holder> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * 
 * ===================================================================================
 * Author: Fan Lin (linfan68@gmail.com)
 * 
 *************************************************************************************/

// Revision 5

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

using Texture = Microsoft.Xna.Framework.Graphics.Texture2D;
//using GoblinXNA.Shaders;
using NOVA.Scenery;
using NOVA.Utilities;

namespace NOVA.Graphics
{
    /// <summary>
    /// A shader that implements the DirectX 9's fixed-pipeline lighting, and there is no limitation
    /// on the number of light sources it can handle. This shader can render directional, point,
    /// and spot lights (the equations used for each light type can be found from 
    /// http://msdn.microsoft.com/en-us/library/bb174697(VS.85).aspx). 
    /// </summary>
    public class UnifiedShader //: Shader
    {
        internal EffectParameter world,
            worldViewProj,
            viewProj,
            projection,
            viewInverse,
            worldForNormal,
            cameraPosition,

            //Material Paramters
            emissiveColor,
            diffuseColor,
            specularColor,
            specularPower,
            colorMapTexture,
            colorMapEnabled,
            normalMapTexture,
            normalMapEnabled,
            heightMapTexture,
            heightMapEnabled,
            Transparency,

            //Light paramters
            lights,
            light,
            numberOfLights,

            ambientLightColor,
            DirLightMix,
            PointLightMix,
            SpotLightMix,

            // Parallax
            scaleBias,

            // Glow
            GlowEnabled,
            GlowColor,
            GlowExponential;

        private Effect effect;

        CameraObject m_camera;

        RenderMaterial m_material;

        private List<LightObject> lightSources;
        private List<LightObject> dirLightSources;
        private List<LightObject> pointLightSources;
        private List<LightObject> spotLightSources;

        private int maxNumLightsPerPass;
        private bool is_3_0;
        private bool forcePS20;

        private Vector3 cameraPos;

        /// <summary>
        /// Creates a Unified shader that uses the 'UnifiedShading.fx' shader file for the embedded content of the NOVA framework. 
        /// </summary>
        public UnifiedShader()
            //: base("UnifiedShading")
        {
            lightSources = new List<LightObject>();
            dirLightSources = new List<LightObject>();
            pointLightSources = new List<LightObject>();
            spotLightSources = new List<LightObject>();

            maxNumLightsPerPass = 12;
            is_3_0 = false;
            forcePS20 = false;

            if (/*(State.Device.GraphicsDeviceCapabilities.PixelShaderVersion.Major >= 3) &&*/ !forcePS20)
            {
                is_3_0 = true;
            }
            else
            {
                is_3_0 = false;
            }

            effect = Core.UnifiedShaderClone; //State.Content.Load<Effect>("UnifiedShading").Clone();

            GetParameters();
        }

        /// <summary>
        /// Gets or sets whether to force this shader to use Pixel Shader 2.0 profile even if
        /// the graphics card support Pixel Shader 3.0. By default, this is set to true.
        /// </summary>
        /// <remarks>
        /// When there are less than fifty lights, it's faster to use Pixel Shader 2.0 profile.
        /// However, if you have more than fifty lights, Pixel Shader 3.0 will perform better.
        /// </remarks>
        public bool UsePS20
        {
            get { return forcePS20; }
            set { forcePS20 = value; }
        }

        public int MaxLights
        {
            get { return 1000; }
        }

        // TO_DELETE
        //private void GetMinimumParameters()
        //{
        //    world = effect.Parameters["world"];
        //    viewProj = effect.Parameters["viewProjection"];
        //    worldForNormal = effect.Parameters["worldForNormal"];
        //    cameraPosition = effect.Parameters["cameraPosition"];

        //    lights = effect.Parameters["lights"];
        //    light = effect.Parameters["light"];
        //    ambientLightColor = effect.Parameters["ambientLightColor"];
        //    numberOfLights = effect.Parameters["numberOfLights"];
        //}

        /// <summary>
        /// Create shortcuts to the shaders parameters.
        /// </summary>
        protected void GetParameters()
        {
            //Binding the effect parameters in to Effect File;

            // Geometry
            world = effect.Parameters["world"];
            viewProj = effect.Parameters["viewProjection"];
            worldForNormal = effect.Parameters["worldForNormal"];
            cameraPosition = effect.Parameters["cameraPosition"];

            // Material
            emissiveColor = effect.Parameters["emissiveColor"];
            diffuseColor = effect.Parameters["diffuseColor"];
            specularColor = effect.Parameters["specularColor"];
            specularPower = effect.Parameters["specularPower"];
            colorMapTexture = effect.Parameters["colorMapTexture"];
            colorMapEnabled = effect.Parameters["colorMapEnabled"];
            normalMapTexture = effect.Parameters["normalMapTexture"];
            normalMapEnabled = effect.Parameters["normalMapEnabled"];
            heightMapTexture = effect.Parameters["heightMapTexture"];
            heightMapEnabled = effect.Parameters["heightMapEnabled"];
            Transparency = effect.Parameters["Transparency"];

            // Lights
            lights = effect.Parameters["lights"];
            light = effect.Parameters["light"];
            ambientLightColor = effect.Parameters["ambientLightColor"];
            numberOfLights = effect.Parameters["numberOfLights"];

            DirLightMix = effect.Parameters["DirectionalLightMix"];
            SpotLightMix = effect.Parameters["SpotLightMix"];
            PointLightMix = effect.Parameters["PointLightMix"];

            // Parallax
            scaleBias = effect.Parameters["scaleBias"];

            // Glow
            GlowEnabled = effect.Parameters["GlowEnabled"];
            GlowColor = effect.Parameters["GlowColor"];
            GlowExponential = effect.Parameters["GlowExponential"];
        }

        /// <summary>
        /// Set the render material parameters.
        /// </summary>
        public void SetParameters(RenderMaterial material)
        {      
            emissiveColor.SetValue(material.Emissive);
            diffuseColor.SetValue(material.Diffuse);
            specularColor.SetValue(material.Specular);
            specularPower.SetValue(material.SpecularPower);

            ambientLightColor.SetValue(new Vector4(1f, 1f, 1f, 1f));

            Transparency.SetValue(material.Transparency);

            if (material.TextureSet)//material.Texture != null)
            {
                colorMapEnabled.SetValue(true);
                colorMapTexture.SetValue(material.Texture);
            }
            else
                colorMapEnabled.SetValue(false);

            if (material.NormalMapSet)
            {
                normalMapEnabled.SetValue(true);
                normalMapTexture.SetValue(material.NormalMap);
            }
            else
                normalMapEnabled.SetValue(false);

            if (material.HeightMapSet)
            {
                heightMapEnabled.SetValue(true);
                heightMapTexture.SetValue(material.HeightMap);
                scaleBias.SetValue(new Vector2(0.04f, -0.03f));
            }
            else
                heightMapEnabled.SetValue(false);

            // Glow
            GlowEnabled.SetValue(material.EnableGlow);
            GlowColor.SetValue(material.GlowColor);
            GlowExponential.SetValue((material.GlowFactor > 0) ? (1f / material.GlowFactor) : 0f);

            m_material = material;
        }

        /// <summary>
        /// Set the camera parameters.
        /// </summary>
        public void SetParameters(CameraObject camera)
        {
            //cameraPos = camera.WorldTransformation.Translation;
            cameraPos = camera.Position;

            cameraPosition.SetValue(cameraPos);
        }

        /// <summary>
        /// Set the lighting parameters.
        /// </summary>
        public void SetParameters(List<LightObject> lights)//List<LightNode> globalLights, List<LightNode> localLights)
        {
            bool ambientSet = false;
            this.lightSources.Clear();
            Vector4 ambientLightColor = new Vector4(0.8f, 0.8f, 0.8f, 1f);
            
            dirLightSources.Clear();
            pointLightSources.Clear();
            spotLightSources.Clear();

            foreach (LightObject l in lights)
            {
                switch (l.Source.Type)
                {
                    case LightType.Directional:
                        if(l.Source.Enabled)
                            dirLightSources.Add(l);
                        break;
                    case LightType.Point:
                        if (l.Source.Enabled)
                            pointLightSources.Add(l);
                        break;
                    case LightType.SpotLight:
                        if (l.Source.Enabled)
                            spotLightSources.Add(l);
                        break;
                }
            }

            this.ambientLightColor.SetValue(ambientLightColor); 

            // Set the mixing factors for the alpha blend, i.e. if all three 
            // light sources exist, then the alpha blend factor should be 1/3 each.
            float coeff = (1f - m_material.Transparency) / (((dirLightSources.Count > 0) ? 1f : 0f) + ((pointLightSources.Count > 0) ? 1f : 0f) + ((spotLightSources.Count > 0) ? 1f : 0f));
            DirLightMix.SetValue((dirLightSources.Count > 0) ? coeff : 0f);
            PointLightMix.SetValue((pointLightSources.Count > 0) ? coeff : 0f);
            SpotLightMix.SetValue((spotLightSources.Count > 0) ? coeff : 0f);
        }

        /// <summary>
        /// Renders the current object
        /// </summary>
        public void Render(Matrix worldMatrix, string techniqueName, RenderHandler renderDelegate, bool bEnableLight)
        {
            if (renderDelegate == null)
                ExceptionManager.Add(new Exception("renderDelegate is null"));

            
            world.SetValue(worldMatrix);
            cameraPosition.SetValue(Vector3.Transform(new Vector3(), Matrix.Invert(EngineSettings.ViewMatrix)));
            viewProj.SetValue(EngineSettings.ViewMatrix * EngineSettings.ProjectionMatrix);//State.ViewProjectionMatrix);
            worldForNormal.SetValue(Matrix.Transpose(Matrix.Invert(worldMatrix)));

            Matrix worldInvTrans = Matrix.Transpose(Matrix.Invert(worldMatrix));

            // Calculate combined world-view-projection matrix.
            Matrix worldViewProj = worldMatrix * EngineSettings.ViewMatrix * EngineSettings.ProjectionMatrix;

            // Set shader matrix parameters.
            //effect.Parameters["worldInverseTransposeMatrix"].SetValue(worldInvTrans);
            //effect.Parameters["worldViewProjectionMatrix"].SetValue(worldViewProj);

            // Start shader
            effect.CurrentTechnique = effect.Techniques["GeneralTextureLighting"];
            {
                DepthStencilState depthStencilState = Core.Device.DepthStencilState;
                BlendState blendState = Core.Device.BlendState;
                //DepthStencilState depthStencilState = EngineSettings.GraphicsDevice.DepthStencilState;
                //BlendState blendState = EngineSettings.GraphicsDevice.BlendState;
                
                
                //EngineSettings.GraphicsDevice.DepthStencilState = DepthStencilState.Default;
                //EngineSettings.GraphicsDevice.BlendState = BlendState.Opaque;
                               
                Core.Device.DepthStencilState = DepthStencilState.Default;
                Core.Device.BlendState = BlendState./*AlphaBlend;*/Opaque;

                effect.CurrentTechnique.Passes["Ambient"].Apply();
                renderDelegate();

                if (bEnableLight)
                {
                    Core.Device.BlendState = BlendState.AlphaBlend;
                    //EngineSettings.GraphicsDevice.BlendState = BlendState.AlphaBlend;

                    if (is_3_0)
                    {
                        DoRendering30(renderDelegate, dirLightSources);
                        DoRendering30(renderDelegate, pointLightSources);
                        DoRendering30(renderDelegate, spotLightSources);
                    }
                    else
                    {
                        DoRendering20(renderDelegate, dirLightSources);
                        DoRendering20(renderDelegate, pointLightSources);
                        DoRendering20(renderDelegate, spotLightSources);
                    }
                }
                
                
                Core.Device.BlendState = blendState;
                Core.Device.DepthStencilState = depthStencilState;
                //EngineSettings.GraphicsDevice.BlendState = blendState;
                //EngineSettings.GraphicsDevice.DepthStencilState = depthStencilState;
            }
        }

        private void DoRendering30(RenderHandler renderDelegate, List<LightObject> lightSources)
        {
            string passName;
            if (lightSources.Count == 0)
            {
                return;
            }
            else
            {
                passName = "Multiple" + GetPassName(lightSources[0].Source.Type);
            }
            for (int passCount = 0; passCount < (((lightSources.Count - 1) / maxNumLightsPerPass) + 1); passCount++)
            {

                int count = 0;
                for (int l = 0; l < maxNumLightsPerPass; l++)
                {
                    int lightIndex = (passCount * maxNumLightsPerPass) + l;
                    if (lightIndex >= lightSources.Count)
                    {
                        break;
                    }

                    SetUpLightSource(lightSources[lightIndex], l);
                    count++;
                }

                numberOfLights.SetValue(count);

                effect.CurrentTechnique.Passes[passName].Apply();
                renderDelegate();
            }
        }

        private void DoRendering20(RenderHandler renderDelegate, List<LightObject> lightSources)
        {
            string passName;
            if (lightSources.Count == 0)
            {
                return;
            }
            else
            {
                passName = "Single" + GetPassName(lightSources[0].Source.Type);
            }
            for (int passCount = 0; passCount < lightSources.Count; passCount++)
            {
                SetUpSingleLightSource(lightSources[passCount]);
                effect.CurrentTechnique.Passes[passName].Apply();
                renderDelegate();
            }
        }

        private void SetUpLightSource(LightObject lightSource, int index)
        {
            lights.Elements[index].StructureMembers["direction"].SetValue(lightSource.Source.Direction);
            lights.Elements[index].StructureMembers["position"].SetValue(lightSource.Source.Position);
            lights.Elements[index].StructureMembers["falloff"].SetValue(lightSource.Source.Falloff);
            lights.Elements[index].StructureMembers["range"].SetValue(lightSource.Source.Range * 2 /* Adapting to Deferred Rendering */);
            lights.Elements[index].StructureMembers["diffuse"].SetValue(lightSource.Source.Diffuse);
            lights.Elements[index].StructureMembers["specular"].SetValue(lightSource.Source.Specular);
            lights.Elements[index].StructureMembers["attenuation0"].SetValue(lightSource.Source.Attenuation0);
            lights.Elements[index].StructureMembers["attenuation1"].SetValue(lightSource.Source.Attenuation1);
            lights.Elements[index].StructureMembers["attenuation2"].SetValue(lightSource.Source.Attenuation2);
            lights.Elements[index].StructureMembers["innerConeAngle"].SetValue(lightSource.Source.InnerConeAngle);
            lights.Elements[index].StructureMembers["outerConeAngle"].SetValue(lightSource.Source.OuterConeAngle);
        }

        private void SetUpSingleLightSource(LightObject lightSource)
        {
            light.StructureMembers["direction"].SetValue(lightSource.Source.Direction);
            light.StructureMembers["position"].SetValue(lightSource.Source.Position);
            light.StructureMembers["falloff"].SetValue(lightSource.Source.Falloff);
            light.StructureMembers["range"].SetValue(lightSource.Source.Range * 2 /* Adapting to Deferred Rendering */);
            light.StructureMembers["color"].SetValue(lightSource.Source.Diffuse);
            light.StructureMembers["attenuation0"].SetValue(lightSource.Source.Attenuation0);
            light.StructureMembers["attenuation1"].SetValue(lightSource.Source.Attenuation1);
            light.StructureMembers["attenuation2"].SetValue(lightSource.Source.Attenuation2);
            light.StructureMembers["innerConeAngle"].SetValue(lightSource.Source.InnerConeAngle);
            light.StructureMembers["outerConeAngle"].SetValue(lightSource.Source.OuterConeAngle);
        }

        static private int compareLightSource(LightSource l1, LightSource l2)
        {
            if ((int)l1.Type > (int)l2.Type)
            {
                return 1;
            }
            else if (l1.Type == l2.Type)
            {
                return 0;
            }
            else
            {
                return -1;
            }
        }

        private string GetPassName(LightType type)
        {
            switch (type)
            {
                case LightType.Directional:
                    return "DirectionalLight";
                case LightType.Point:
                    return "PointLight";
                case LightType.SpotLight:
                    return "SpotLight";
            }
            return null;
        }

        public void Dispose()
        {
            lightSources.Clear();
            dirLightSources.Clear();
            pointLightSources.Clear();
            spotLightSources.Clear();
        }
    }
}
